4-1 头部功能性组件:暗黑模式
1. 暗黑模式概述
管理后台的顶部导航栏通常会包含一系列功能性组件,暗黑模式(Dark Mode)是最基础也是最常见的功能之一。它允许用户在浅色和深色主题之间切换,适用于不同的使用环境(如夜间浏览)。
暗黑模式的实现方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| CSS 变量切换 | 通过 JS 切换 html 上的 class,配合 CSS 变量实现主题色变化 | 灵活可控,支持过渡动画 | 需要手动维护 CSS 变量映射 | 自定义主题系统 |
VueUse useDark | 封装了 usePreferredDark + useStorage,自动持久化 | 开箱即用,自动跟随系统 | 切换时可能导致页面重渲染,丢失过渡效果 | 快速集成 |
prefers-color-scheme 媒体查询 | 纯 CSS 方案,跟随系统设置 | 零 JS 开销 | 无法让用户手动切换 | 仅跟随系统 |
| CSS-in-JS 动态主题 | 运行时生成主题变量 | 最灵活 | 性能开销大 | 复杂主题系统 |
本节采用方案:以 VueUse
usePreferredDark感知系统偏好 +useStorage持久化用户选择 + 手动控制darkclass 添加/移除,确保过渡动画不被破坏。
2. Element Plus 暗黑模式集成
Element Plus 从 2.0 版本开始原生支持暗黑模式,核心原理是提取所有设计变量并通过 CSS 变量(CSS Variables)实现动态主题更新。
2.1 导入暗黑模式 CSS 变量
在 main.ts 中导入 Element Plus 暗黑模式的 CSS 变量文件:
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 导入 Element Plus 暗黑模式 CSS 变量
import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
typescript
2.2 自定义暗黑模式 CSS 变量
如果需要覆盖 Element Plus 的默认暗黑模式颜色,创建自定义 CSS 文件:
/* styles/dark/css-vars.css */
html.dark {
/* 自定义深色背景颜色 */
--el-bg-color: #141414;
--el-bg-color-page: #0a0a0a;
--el-bg-color-overlay: #1d1e1f;
/* 自定义文字颜色 */
--el-text-color-primary: #e5eaf3;
--el-text-color-regular: #cfd3dc;
/* 自定义边框颜色 */
--el-border-color: #4c4d4f;
--el-border-color-light: #414243;
}
css
然后在 main.ts 中将其导入在 Element Plus 暗黑模式 CSS 之后:
import 'element-plus/theme-chalk/dark/css-vars.css'
import './styles/dark/css-vars.css' // 必须在 Element Plus CSS 之后导入
typescript
2.3 暗黑模式触发原理
Element Plus 的暗黑模式生效条件非常简单 -- 在 <html> 标签上添加 dark class:
<!-- 暗黑模式开启 -->
<html class="dark">
...
</html>
<!-- 暗黑模式关闭 -->
<html>
...
</html>
html
3. VueUse 核心 Composable 详解
3.1 useDark -- 一键暗黑模式
useDark 是 VueUse 提供的暗黑模式 composable,它内部组合了 usePreferredDark 和 useStorage:
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
typescript
内部工作机制:
- 初始化时检查
localStorage/sessionStorage中是否有用户偏好 - 如果没有存储的偏好,则回退到系统偏好(
usePreferredDark) - 修改
isDark的值时,自动更新目标元素(默认<html>)的属性并持久化到存储
3.2 usePreferredDark -- 感知系统暗黑偏好
usePreferredDark 通过 prefers-color-scheme 媒体查询响应式地获取系统的暗黑模式偏好:
import { usePreferredDark } from '@vueuse/core'
const prefersDark = usePreferredDark()
// 返回 Ref<boolean>,响应式跟踪系统暗黑模式设置
typescript
CSS 媒体查询等价写法:
/* 浅色模式(默认) */
.element {
background-color: #ffffff;
color: #333333;
}
/* 深色模式 -- 跟随系统 */
@media (prefers-color-scheme: dark) {
.element {
background-color: #1a1a1a;
color: #e0e0e0;
}
}
css
3.3 useStorage -- 持久化用户偏好
useStorage 是 VueUse 提供的响应式 localStorage 封装:
import { useStorage } from '@vueuse/core'
// 参数:key, 默认值, 存储对象(默认 localStorage)
const darkModeFlag = useStorage('dark-mode-flag', undefined)
typescript
4. 暗黑模式切换组件开发
4.1 问题分析:为什么不用 useDark 直接切换
直接使用 useDark 会导致 isDark 的值立即改变,触发 Vue 的响应式更新,使所有依赖 isDark 的组件重新渲染。这会导致:
- Switch 组件的过渡动画丢失
- 页面背景色的平滑过渡效果消失
- 整个页面"闪烁"切换而非"渐变"切换
解决方案:不直接使用 useDark 的自动切换功能,而是手动控制 dark class 的添加与移除,通过 CSS transition 实现平滑过渡。
4.2 完整组件实现
src/components/things/
└── DarkModeToggle.vue
text
<!-- src/components/things/DarkModeToggle.vue -->
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { usePreferredDark, useStorage } from '@vueuse/core'
// Props: 允许外部传入默认暗黑模式状态
const props = withDefaults(defineProps<{
dark?: boolean
}>(), {
dark: undefined,
})
// 使用 useStorage 持久化用户选择,key 为 'dark-mode-flag'
const isDark = useStorage<boolean | undefined>('dark-mode-flag', undefined)
// 感知系统暗黑模式偏好
const prefersDark = usePreferredDark()
// 切换暗黑模式的核心方法
function toggleMode(flag: boolean) {
if (flag) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
// 监听用户手动切换
watch(isDark, (val) => {
if (val !== undefined) {
toggleMode(val)
}
}, { immediate: true })
// 监听系统暗黑偏好变化
watch(prefersDark, (val) => {
// 只有用户没有手动设置过时,才跟随系统
if (isDark.value === undefined) {
toggleMode(val)
}
})
// 组件挂载时初始化
onMounted(() => {
if (props.dark !== undefined) {
// 外部传入优先
isDark.value = props.dark
} else if (isDark.value === undefined) {
// 用户未设置过,使用系统偏好
isDark.value = prefersDark.value
}
})
</script>
<template>
<el-switch
v-model="isDark"
:active-action-icon="() => h('i', { class: 'i-prime:moon' })"
:inactive-action-icon="() => h('i', { class: 'i-prime:sun' })"
style="--el-switch-on-color: #333"
/>
</template>
vue
注意:如果使用 TSX 语法(文件后缀为
.tsx),图标可以使用 JSX 形式:<i class="i-prime:moon" />。在.vue文件中,需使用h()渲染函数。
4.3 JSX 版本(可选)
如果项目配置了 JSX 支持,可以使用 .tsx 格式,图标写法更直观:
// src/components/things/DarkModeToggle.tsx
import { defineComponent, ref, watch, onMounted } from 'vue'
import { usePreferredDark, useStorage } from '@vueuse/core'
export default defineComponent({
name: 'DarkModeToggle',
props: {
dark: {
type: Boolean,
default: undefined,
},
},
setup(props) {
const isDark = useStorage<boolean | undefined>('dark-mode-flag', undefined)
const prefersDark = usePreferredDark()
function toggleMode(flag: boolean) {
if (flag) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
watch(isDark, (val) => {
if (val !== undefined) {
toggleMode(val)
}
}, { immediate: true })
watch(prefersDark, (val) => {
if (isDark.value === undefined) {
toggleMode(val)
}
})
onMounted(() => {
if (props.dark !== undefined) {
isDark.value = props.dark
} else if (isDark.value === undefined) {
isDark.value = prefersDark.value
}
})
return () => (
<el-switch
v-model={isDark.value}
active-action-icon={() => <i class="i-prime:moon" />}
inactive-action-icon={() => <i class="i-prime:sun" />}
style={{ '--el-switch-on-color': '#333' }}
/>
)
},
})
tsx
5. 过渡动画配置
5.1 全局 CSS 过渡
在 main.css 中为 <html> 元素添加过渡效果:
/* main.css */
/* 浅色模式默认背景 */
html {
background-color: var(--el-bg-color, #ffffff);
transition: background-color 0.3s ease-out;
}
/* 暗黑模式背景 */
html.dark {
background-color: var(--el-bg-color, #141414);
transition: background-color 0.3s ease-out;
}
css
关键点:使用 Element Plus 的 CSS 变量
--el-bg-color作为背景色,确保与 Element Plus 组件的背景色保持一致。
5.2 自定义组件的暗黑模式适配
在 UnoCSS 中,使用 dark: 前缀来编写暗黑模式下的样式:
<template>
<div class="w-40 h-40 bg-red-500 dark:bg-sky-500">
自定义块元素
</div>
</template>
vue
UnoCSS dark 变体工作原理:
| 配置项 | 生成的 CSS | 切换方式 |
|---|---|---|
dark: 'class'(默认) | .dark .dark\:bg-sky-500 { ... } | 在 <html> 添加 dark class |
dark: 'media' | @media (prefers-color-scheme: dark) { ... } | 跟随系统设置 |
重要说明:当项目使用了 presetWind(或 presetWind3)时,presetMini 的 dark class 配置作为子集自动生效,无需额外配置。这就是为什么 dark: 前缀可以直接使用的原因。
6. 图标集成(UnoCSS Icons)
6.1 图标搜索与使用
UnoCSS 集成了 Iconify 图标集,可以直接在 class 中使用图标:
<!-- 使用 Iconify 图标集 -->
<i class="i-prime:moon" /> <!-- 月亮图标 -->
<i class="i-prime:sun" /> <!-- 太阳图标 -->
html
6.2 safelist 配置
对于在动态组件中使用的图标,需要将其加入 safelist,确保 UnoCSS 编译时能正确打包这些图标样式:
// uno.config.ts
import { defineConfig, presetIcons } from 'unocss'
export default defineConfig({
presets: [
presetIcons({
scale: 1.2,
warn: true,
}),
],
// 将动态使用的图标加入 safelist
safelist: [
'i-prime:moon',
'i-prime:sun',
],
})
typescript
7. 优先级体系
暗黑模式的最终状态由三个因素决定,优先级从高到低:
用户手动设置(useStorage/localStorage) > 组件 Props 传入 > 系统偏好(prefers-color-scheme)
text
┌─────────────────────────────────────┐
│ 用户手动切换 (localStorage) │ ← 最高优先级
│ isDark.value = true/false │
├─────────────────────────────────────┤
│ 组件 Props 传入 │ ← 中等优先级
│ <DarkModeToggle :dark="true" /> │
├─────────────────────────────────────┤
│ 系统偏好 (prefers-color-scheme) │ ← 最低优先级(回退)
│ prefersDark.value │
└─────────────────────────────────────┘
text
8. 在页面中使用
<!-- src/pages/index.vue -->
<script setup lang="ts">
import DarkModeToggle from '~/components/things/DarkModeToggle.vue'
</script>
<template>
<div>
<!-- 其他头部内容 -->
<!-- 暗黑模式切换 -->
<DarkModeToggle />
<!-- 或者传入默认值 -->
<!-- <DarkModeToggle :dark="true" /> -->
</div>
</template>
vue
9. 完整流程总结
1. main.ts 导入 Element Plus 暗黑 CSS 变量
└─ import 'element-plus/theme-chalk/dark/css-vars.css'
2. main.css 配置过渡动画
└─ html { transition: background-color 0.3s ease-out; }
3. 创建 DarkModeToggle.vue 组件
├─ useStorage 持久化用户选择
├─ usePreferredDark 感知系统偏好
├─ watch 监听变化并手动切换 dark class
└─ el-switch + 图标组成 UI
4. onMounted 初始化逻辑
├─ Props 传入 → 最高优先级
├─ localStorage 已存储 → 用户历史选择
└─ 系统偏好 → 回退方案
5. UnoCSS dark: 前缀适配自定义组件
└─ <div class="bg-white dark:bg-gray-800">
text
关键词
useDark/usePreferredDark/useStorage-- VueUse 暗黑模式相关 composableelement-plus/theme-chalk/dark/css-vars.css-- Element Plus 暗黑模式 CSS 变量dark:前缀 -- UnoCSS 暗黑模式变体prefers-color-scheme-- CSS 媒体查询,检测系统暗黑偏好- CSS Variables -- 实现主题切换的核心技术
transition-- CSS 过渡动画,实现平滑切换效果
↑